#include "utils.h"
#include "clone_detect.h"
#include "cjson.h"

duplicate_result_t dup_result;

options_t global_options;

bool record( src_file_t *src1, src_file_t *src2, unsigned int src1_start, unsigned int src1_end, unsigned int src2_start, unsigned int src2_end )
{
    result_t *result = &dup_result.results[ dup_result.result_num ];
    dup_detail_t *detail;

    if( result->dup_detail_num == MAX_DETAIL_FOR_EACH_RESULT )
    {
        DEBUG("EXCEED THE DUPLICATE DETAIL LIMIT BETWEEN FILE: %d\n", result->dup_detail_num );
        return false;
    }

    if( src1_start >= src1_end || src2_start >= src2_end
        || src1_start >= MAX_LINE || src1_end >= MAX_LINE 
        || src2_start >= MAX_LINE || src2_end >= MAX_LINE )
    {
        DEBUG("INVALID RECORD PAIR: ( %d, %d ), (%d, %d)\n", src1_start, src1_end, src2_start, src2_end );
        return false;

    }

    detail = &result->details[ result->dup_detail_num ];

    result->src1_name = src1->file_name;
    result->src2_name = src2->file_name;
    detail->src_startpoint = src1->file_content[ src1_start ].line_no;
    detail->src_startpoint2 = src2->file_content[ src2_start ].line_no;
    detail->length1 = src1->file_content[ src1_end ].line_no - src1->file_content[ src1_start ].line_no + 1;
    detail->length2 = src2->file_content[ src2_end ].line_no- src2->file_content[ src2_start ].line_no + 1;

    result->dup_line += detail->length1 + detail->length2;
    dup_result.total_line += detail->length1 + detail->length2;

    DEBUG("detail FIND: %d, %d, %d, %d\n", detail->src_startpoint, detail->src_startpoint2, detail->length1, detail->length2 );
    result->dup_detail_num++;

    return true;
}

void dump_file( src_file_t *src_file )
{
    DEBUG("***************DUMP SIMPLE %s********************\n", src_file->file_name );
    for( unsigned int i = 0; i < src_file->simplify_line_no; i++ )
    {
        DEBUG( "%s\n", src_file->file_content[i].line );
    }
    DEBUG("***************DUMP       END********************\n");

}


/**
* @brief Loop the input directory and read source code file, current only support process 2 srouce files 
*
* @param dir_name               directory read from command arguments
* @param src_file_list          read SRC_FILE_NUM code files from dir
*
* @return the number of files read from directory 
*/
unsigned int read_dir( char * dir_name , src_file_list_t *src_file_list )
{
    char file_path[MAX_FILE_NAME_LEN];
    DIR *dir = opendir( dir_name );
    if ( NULL == dir )
    {
        ERROR("OPEN SROUCE DIR %s FAILED\n", dir_name );
        return src_file_list->file_num;
    }

    if( dir_name[strlen(dir_name) - 1] == '/')
        dir_name[strlen(dir_name) - 1] = '\0';

    struct dirent *ent = readdir( dir );
    while( ent )
    {
        if( ent->d_type == DT_DIR )
        {
            if( ent->d_name[0] != '.' )
            {
                DEBUG("dir %s\n", ent->d_name );
                sprintf( file_path, "%s/%s", dir_name, ent->d_name );
                read_dir( file_path , src_file_list );
            }

        }
        else if( ent->d_type == DT_REG )
        {
            if( iscodefile( ent->d_name ))
            {
                sprintf( file_path, "%s/%s", dir_name, ent->d_name );
                strncpy( src_file_list->src_file [ src_file_list->file_num++ ].file_name, file_path , MAX_FILE_NAME_LEN );
                DEBUG("source file %s\n", src_file_list->src_file[ src_file_list->file_num - 1 ].file_name);
            }
        }

        if( src_file_list->file_num >= SRC_FILE_NUM )
        {
            break;
        }

        ent = readdir( dir );
    }
    closedir( dir );

    return src_file_list->file_num;
}

bool process_file( src_file_t *src_file )
{
    FILE *src_fp = NULL;
    char line[MAX_LEN] = { 0 };
    unsigned int line_no = 0; // count for line number
    char simplify_line[MAX_LEN] = { 0 };
    unsigned int simplify_line_no = 0; // count for line number which is simplified, delete blanks and others like comments if -b is given
    char *p = NULL;
    bool multiple_line_comment = false;

    src_fp = fopen( src_file->file_name, "r" );

    if( NULL == src_fp )
    {
        DEBUG( "open file fail\n" );
        return false;
    }

    while( fgets( line, MAX_LINE, src_fp ))
    {
        line_no ++;
        p = line;
        /* remove beginning whitespace */
        while( iswhitespace( *p ))
            p++;

        /* remove blank line */
        if( strlen(p) == 0 )
            continue;

        /* process comment */ 
        if( global_options.ignore_comments )
        {
            if( iscomments( p, &multiple_line_comment))
                continue;
        }

        /* remove extra whilespace */
        str_rm_extra_wsp( p, simplify_line );
        DEBUG("simplify file %s\n", simplify_line );

        /* process brace */
        /* try link table in furture */
        size_t len = strlen( simplify_line );
        if( global_options.ignore_braces && isendwithbrace( simplify_line ))
        {
            src_file->file_content[simplify_line_no].line_no = line_no;
            strncpy( src_file->file_content[ simplify_line_no ].line, simplify_line , len - 1 );
            simplify_line_no++;
            src_file->file_content[simplify_line_no].line_no = line_no;
            strcpy( src_file->file_content[simplify_line_no].line, &simplify_line[ len - 1 ] );
            simplify_line_no++;

        }
        else
        {
            src_file->file_content[simplify_line_no].line_no = line_no;
            strncpy( src_file->file_content[simplify_line_no].line, simplify_line, MAX_LEN );
            simplify_line_no++;

        }

        if( simplify_line_no >= MAX_LINE - 1 )
        {
            DEBUG("Current support simplied file length %d\n", MAX_LINE );
            break;
        }

    }
    src_file->simplify_line_no = simplify_line_no;
    DEBUG("line number %d\n", line_no);

    /* if debug on, print file for debug */
    dump_file( src_file );
    return true;

}

/* print for debug */
void print_match_matrix( bool *match_matrix , unsigned int n, unsigned int m )
{
    unsigned int i = 0, j = 0;
    DEBUG("matrix(%d, %d)\n", n, m);

    for (; i < n; i++ )
    {
        DEBUG("[%2d] ", i );
        for ( j = 0; j < m; j++ )
        {
            printf("%d", match_matrix[ i * m + j ] );
        }
        printf("\n");
    }
}


/**
 * @brief Search whether dup line >= limit exist
 *        The search is start from (i, j)  point in the matrix
 *        And compare following line as (i + 1, j + 1) ... ( i + n, j + n )
 *
 * @param match_matrix the matrix record the comparison of each line bewteen source file
 * @param src1 source file
 * @param src2 source file
 * @param i    start search line in src1
 * @param j    start search line in src2
 *
 * @return  whether duplicate found
 */
bool find_dup_len( bool *match_matrix , src_file_t *src1, src_file_t *src2 , unsigned int i, unsigned int j )
{
    unsigned int len = 0;
    unsigned int start_i = 0, start_j = 0;
    bool match_found = false;
    for(; i < src1->simplify_line_no && j < src2->simplify_line_no ; i++, j++)
    {
        if( match_matrix[ i * src2->simplify_line_no + j ])
        {
            if( len == 0)
            {
                start_i = i;
                start_j = j;
            }
            len++;
        }
        else
        {
            if( len >= global_options.min_dup_line )
            {
                DEBUG("match find %d  ( %d, %d )\n", len, src1->file_content[start_i].line_no, src2->file_content[start_j].line_no );
                if( !record(src1, src2, start_i, i - 1, start_j, j - 1 ) )
                    break;
                match_found = true;
            }

            len = 0;
        }

    }
    if( len >= global_options.min_dup_line )
    {
        DEBUG("match find %d  ( %d, %d )\n", len, src1->file_content[start_i].line_no, src2->file_content[start_j].line_no );
        if( record( src1, src2, start_i, i - 1, start_j, j - 1 ))
            match_found = true;
    }
    
    return match_found;

}

/**
* @brief check duplicate len based on compare matrix
*
* @param match_matrix  contains each line compare result
* @param src1          source file
* @param src2          source file
*/
bool check_match_matrix( bool *match_matrix , src_file_t *src1, src_file_t *src2 )
{

    unsigned int i, j ,k = 0, len = 0;
    bool match_found = false;

    if( global_options.verbose )
        print_match_matrix( match_matrix, src1->simplify_line_no, src2->simplify_line_no );

    for( k = 0; k < src2->simplify_line_no; k++ )
    {
        i = 0; j = k; len = 0;
        if (find_dup_len( match_matrix, src1, src2 , i, j ))
            match_found = true;
    }
    // j = 0, i loop
    for ( k = 1; k < (src1->simplify_line_no - 1); k++ )
    {
        i = k; j = 0; len = 0;
        if (find_dup_len( match_matrix, src1, src2 , i, j ))
            match_found = true;
    }

    return match_found;
}

bool detect_between_files( src_file_t *src1, src_file_t *src2 )
{
    unsigned int i = 0, j = 0;
    bool match_found = false;

    DEBUG("detect_between_files %s , %d; %s, %d\n", src1->file_name, src1->simplify_line_no, src2->file_name, src2->simplify_line_no );
    /* use 1-dimension simulate */
    bool *match_matrix = malloc( sizeof(bool) * src1->simplify_line_no * src2->simplify_line_no );

    memset( match_matrix, 0, sizeof(bool) * src1->simplify_line_no * src2->simplify_line_no);
    
    if( strcmp( src1->file_name , src2->file_name ) == 0 )
    {
        DEBUG("detect same file\n");
        for ( i = 0; i < src1->simplify_line_no; i++ )
        {
            for ( j = i + 1; j < src2->simplify_line_no; j++ )

            {
                if( strcmp( src1->file_content[i].line, src2->file_content[j].line ) == 0 )
                {
                    match_matrix[ i * src2->simplify_line_no + j ] = true;
                }
            }
        }
    }else
    {
        for ( i = 0; i < src1->simplify_line_no; i++ )
        {
            for ( j = 0; j < src2->simplify_line_no; j++ )

            {
                if( strcmp( src1->file_content[i].line, src2->file_content[j].line ) == 0 )
                {
                    match_matrix[ i * src2->simplify_line_no + j ] = true;
                }
            }
        }
    }

    match_found = check_match_matrix( match_matrix , src1, src2 );

    free(match_matrix);

    return match_found;
}

bool json_output()
{
    cJSON *json_root = NULL;
    json_root = cJSON_CreateObject();
    if( NULL == json_root )
    {
        return false;
    }

    cJSON_AddNumberToObject ( json_root, "totalDuplicate", dup_result.total_line );

    cJSON *json_results = cJSON_CreateArray();
    cJSON_AddItemToObject( json_root, "results", json_results );
    if( NULL == json_results )
    {
        cJSON_Delete( json_root );
        return false;
    }

    for( unsigned int i = 0; i < dup_result.result_num ; i++ )
    {
        result_t result = dup_result.results[i];
        cJSON *json_result = cJSON_CreateObject();
        if( NULL == json_result )
        {
            cJSON_Delete( json_root );
            return false;
        }

        cJSON_AddItemToArray( json_results, json_result );
        
        cJSON_AddNumberToObject( json_result, "duplicate", result.dup_line );
        cJSON_AddStringToObject( json_result, "fileName1", result.src1_name );
        cJSON_AddStringToObject( json_result, "fileName2", result.src2_name );
        cJSON *json_details = cJSON_CreateArray();
        if( NULL == json_details )
        {
            cJSON_Delete( json_root );
            return false;
        }
        cJSON_AddItemToObject( json_result, "details", json_details );

        for ( unsigned int j = 0; j < result.dup_detail_num; j++ )
        {
            cJSON *json_detail = cJSON_CreateObject();
            if( NULL == json_detail )
            {
                cJSON_Delete( json_root );
                return false;
            }

            cJSON_AddItemToArray( json_details, json_detail);
            cJSON_AddNumberToObject( json_detail, "duplicate", result.details[j].length1 + result.details[j].length2 );
            cJSON_AddNumberToObject( json_detail, "startLine1", result.details[j].src_startpoint );
            cJSON_AddNumberToObject( json_detail, "lineNumber1", result.details[j].length1 );
            cJSON_AddNumberToObject( json_detail, "startLine2", result.details[j].src_startpoint2 );
            cJSON_AddNumberToObject( json_detail, "lineNumber2", result.details[j].length2 );

        }
    }

    char *p = cJSON_Print( json_root );
    cJSON_Delete( json_root );
    DEBUG("%s\n", p );

    FILE *fp = NULL;
    if( ( fp = fopen( global_options.json_file, "w+" )) == NULL )
    {
        DEBUG("FILE OPEN ERROR %s\n", strerror( errno ));
        return false;
    }

    fwrite( p, strlen(p), 1, fp );
    fclose( fp );
    free( p );

    return true;
}

void output_result()
{
    if( global_options.json_output )
    {
        if( !json_output())
        {
            ERROR("JSON OUTPUT FAILED, totalDuplicate: %d\n", dup_result.total_line );
        }
    }
    else
    {
        printf("totalDuplicate: %d\n", dup_result.total_line );
    }
}

void clone_detect( src_file_list_t *src_file_list)
{
    /* detect clone bewteen files and in single file itself */
    for( int i = 0; i < SRC_FILE_NUM; i++ )
        for( int j = i; j < SRC_FILE_NUM; j++ )
            if( detect_between_files( &src_file_list->src_file[i], &src_file_list->src_file[j]))
            {
                /* match found , and dup_result number need add ,
                 * no need check limit MAX_RESULTS now, as the loop will not exceed MAX_RESULT */
                dup_result.result_num++;
            }

    output_result();
}


bool read_args( int argc, char *argv[] )
{
    int opt;

    if( argc < 2 )
    {
        return false;
    }

    strncpy( global_options.dir , argv[1], MAX_FILE_NAME_LEN );

    optind = 2;
    while(( opt = getopt( argc, argv, "cbj:n:v")) != - 1 )
    {
        switch( opt )
        {
            case 'c':
                global_options.ignore_comments = true;
                break;
            case 'b':
                global_options.ignore_braces = true;
                break;
            case 'j':
                global_options.json_output = true;
                strncpy( global_options.json_file, optarg, MAX_FILE_NAME_LEN );
                break;
            case 'n':
                DEBUG("DETECT DUPLICATE LINE >=  %s\n", optarg );
                global_options.min_dup_line = (unsigned int)atoi(optarg);
                break;
            case 'v':
                global_options.verbose = true;
                break;
            default:
                return false;
        }
    }
    return true;
}

#ifndef CPPUTEST
static void print_help()
{
    printf("************************************************************\n");
    printf("Usage: clone_detect src_dir [cbj:n:vh]\n");
    printf("-c: ignore comments when detection duplicate code\n");
    printf("-b: ignore brace format when detection duplicate code\n");
    printf("-j output_file: output duplicate details into a json format file\n");
    printf("-n duplicate_line: set duplicate min limitation when checking\n");
    printf("-v: print verbose log\n");
    printf("************************************************************\n");
}

int main( int argc, char *argv[])
{

    unsigned int i = 0;
    src_file_list_t src_file_list;
    memset( &src_file_list, 0, sizeof( src_file_list_t ));
    memset( &global_options, 0, sizeof( options_t ));
    global_options.min_dup_line = 5;

    if( !read_args( argc, argv ))
    {
        ERROR("COMMAND INPUT ERROR\n");
        print_help();
        return -1;

    }

    if( read_dir( global_options.dir , &src_file_list ) != SRC_FILE_NUM )
    {
        ERROR("NO ENOUGH SOURCE FILE, CURRENT PROCESSING FILE NEED %d\n", SRC_FILE_NUM );
        return -1;
    }

    while( i < src_file_list.file_num )
    {
        if( !process_file( &src_file_list.src_file[i] ))
        {
            ERROR("PRE PROCESS SOURCE CODE FILE FAILED %s\n", src_file_list.src_file[i].file_name );
            return -1;
        }
        i++;
    }

    clone_detect( &src_file_list );

    return 0;
}
#endif
